7.05. Справочник по GraphQL
Справочник по GraphQL
📘 1. Типы в GraphQL (Types)
1.1. Скалярные типы (Scalar Types)
Стандартные (built-in scalars)
| Имя | Описание | Примеры допустимых значений | Примечания |
|---|---|---|---|
Int | 32-битное целое со знаком | 42, -7, 0 | Диапазон: от −2³¹ до 2³¹−1 (−2147483648 … 2147483647). Не поддерживает Infinity, NaN. |
Float | 64-битное число с плавающей точкой (IEEE 754 double) | 3.14, -0.001, 2.998e8 | Поддерживает Infinity, -Infinity, NaN — но большинство реализаций сервера не рекомендуют их передачу (не сериализуются в JSON без кастомного скаляра). |
String | UTF-8 строка | "hello", "", "\\n" | Поддерживает escape-последовательности: \", \\, \/, \b, \f, \n, \r, \t, \uXXXX. |
Boolean | Логическое значение | true, false | Строчные, без кавычек. |
ID | Уникальный идентификатор (сериализуется как String, но семантически уникален и неинтерпретируем) | "abc123", "100" | Используется для кэширования, refetching. Может быть числом, но сериализуется как строка. |
Пользовательские скаляры (Custom Scalars)
Определяются через scalar <Name>. Обязательно должны быть реализованы:
serialize(value)— преобразует внутреннее значение в JSON-совместимое (для ответа).parseValue(value)— преобразует значение из AST (аргументов переменных).parseLiteral(ast)— преобразует значение из AST (литералов в запросе).
Примеры распространённых кастомных скаляров:
DateTime(ISO 8601)JSON(произвольные JSON-объекты/массивы)Decimal,Long,BigIntURL,Email,UUID
⚠️ В SDL нельзя указать семантику кастомного скаляра — только имя. Спецификации поведения передаются через
@specifiedBy(url: "...").
1.2. Составные типы (Composite Types)
1.2.1. Object
type User {
id: ID!
name: String
email: String @deprecated(reason: "Use contact instead")
contact: ContactType
posts(limit: Int = 10): [Post!]!
}
Свойства объекта как типа:
- Имя:
User— должно начинаться с заглавной буквы (по соглашению, не по строгой спецификации, но валидаторы могут ругаться). - Поля: не пустой набор полей (минимум 1).
- Поле состоит из:
- Имени (
id,nameи т.д.) - Типа возврата (обязательно)
- Аргументов (опционально)
- Директив (опционально)
- Имени (
Ограничения:
- Имя поля должно соответствовать
/[_A-Za-z][_0-9A-Za-z]*/. - Имена полей уникальны.
- Циклические зависимости в типах разрешены (например,
User → [Post!],Post → author: User).
1.2.2. Input Object
input UserInput {
name: String!
email: String
tags: [String!] = []
}
Особенности:
- Все поля должны иметь тип (не могут быть интерфейсами или юнионами).
- Поддерживает значения по умолчанию (только для скаляров и ENUM, массивов/вложенных input-объектов — через литералы).
- Примеры валидных значений по умолчанию:
= "default"= 42= true= []= ["a", "b"]= { name: "anon" }— только если вложенныйinputтоже поддерживает такие литералы.
❗
nullне может быть значением по умолчанию в SDL. Если полеnullable, отсутствие значения →null.
1.2.3. Interface
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String
}
type Post implements Node {
id: ID!
title: String
}
Требования к реализации (implements):
- Каждый тип, реализующий интерфейс, обязан содержать все поля интерфейса с точно совпадающими:
- Именем
- Аргументами (имена, типы, обязательность, дефолты)
- Типом возврата (включая
!,[])
- Дополнительные поля разрешены.
- Поле интерфейса может возвращать тип, реализующий интерфейс (самореференция разрешена).
Выполнение:
- При запросе к полю интерфейсного типа (например,
node(id: "...") { ... }) сервер должен указать__typename, чтобы клиент мог применить фрагменты. __typename— обязательное поле во всех объектах и интерфейсах.
1.2.4. Union
union SearchResult = User | Post | Comment
Ограничения:
- Состоит только из Object Types (не интерфейсы, не скаляры, не input).
- Типы в union должны быть уникальны.
- Не может быть пустым.
Использование в запросе:
{
search(query: "foo") {
__typename
... on User { name }
... on Post { title }
}
}
❗ Нельзя запрашивать поля напрямую из union-типа — только через inline-фрагменты с
on.
1.2.5. Enum
enum UserRole {
MEMBER @deprecated
ADMIN
OWNER
}
Свойства:
- Имя:
/[_A-Za-z][_0-9A-Za-z]*/, заглавные — по соглашению. - Значения («значения перечисления»):
/[_A-Za-z][_0-9A-Za-z]*/. - Поддерживает директивы (
@deprecatedи др.). - Сериализуется как строка (
"ADMIN"), но в коде сервера — как enum-значение.
⚠️ Значение
nullне является валидным значением enum. Если нужно — делайтеUserRolenullable:role: UserRole.
1.3. Обёрточные типы (Wrapping Types)
| Синтаксис | Значение | Пример | Примечания |
|---|---|---|---|
T | nullable | String | Может быть null или значением типа T. |
T! | non-null | String! | Обязательно значение: не может быть null. Если resolver возвращает null — ошибка выполнения. |
[T] | nullable list | [String] | Может быть null, [null], ["a"], []. |
[T]! | non-null list | [String]! | Список обязателен, но элементы могут быть null: [null, "a"] — валидно. |
[T!] | nullable list of non-null | [String!] | Список может быть null, но если не null — элементы не могут: ["a"], [], null — валидны; [null] — ошибка. |
[T!]! | non-null list of non-null | [String!]! | Список обязателен, и каждый элемент обязателен: ["a"], [] — валидны; null, [null] — ошибки. |
🔍 При валидации аргументов переменных: если переменная объявлена как
String!, а в operation variables переданоnull— ошибка на этапе проверки переменных (до выполнения).
📘 2. Директивы (Directives)
Директивы — это декларативные аннотации в SDL или запросах, влияющие на поведение выполнения или интроспекции. Состоят из @имя(аргументы...).
2.1. Стандартные (built-in) директивы (обязательны в любой реализации)
| Директива | Применяется к | Аргументы | Описание |
|---|---|---|---|
@include(if: Boolean!) | поля, фрагменты (в запросе) | if: Boolean! | Включает элемент в запрос, если if == true. |
@skip(if: Boolean!) | поля, фрагменты (в запросе) | if: Boolean! | Исключает элемент из запроса, если if == true. |
@deprecated(reason: String) | поля, аргументы, enum значения (в SDL) | reason: String (опциональный) | Помечает элемент как устаревший. Появляется в интроспекции через isDeprecated и deprecationReason. |
@specifiedBy(url: String!) | скалярные типы (в SDL) | url: String! | Указывает URL спецификации для кастомного скаляра (например, https://www.ietf.org/rfc/rfc3339.txt для DateTime). |
⚠️
@includeи@skipне могут применяться к аргументам, типам, полям в SDL — только в операциях и фрагментах.
Примеры использования:
query UserQuery($withPosts: Boolean!) {
user(id: "1") {
id
name
posts @include(if: $withPosts) {
title
}
}
}
type User {
id: ID!
legacyEmail: String @deprecated(reason: "Use contact.email instead")
}
scalar DateTime @specifiedBy(url: "https://www.ietf.org/rfc/rfc3339.txt")
2.2. Распространённые кастомные директивы (не входят в спецификацию, но широко используются)
| Директива | Контекст | Описание | Где встречается |
|---|---|---|---|
@defer(label: String, if: Boolean) | поля (в запросе) | Откладывает выполнение поля и возвращает initial response + последующие инкрементальные ответы (Incremental Delivery). Требует поддержки на сервере (Apollo, Envelop, GraphQL-JS ≥16.6). | Apollo, Relay, Envelop (spec: RFC) |
@stream(initialCount: Int!, label: String, if: Boolean) | поля списка ([T]) | Аналогично @defer, но для потоковой передачи элементов списка по частям. | То же, что выше |
@link(url: String!, as: String, import: [ImportSpec!], for: LinkPurpose) | SDL (в схеме) | Объявляет использование спецификации (например, @tag, @shareable, @inaccessible) в Federation 2. | Apollo Federation 2 |
@tag(name: String!) | SDL (поля, типы) | Помечает элементы для использования в composition (Federation). | Federation 2 |
@shareable | SDL (поля интерфейсов и объектов) | Разрешает дублирование поля в нескольких подграфах. | Federation 2 |
@inaccessible | SDL | Скрывает тип/поле из API, но сохраняет для composition. | Federation 2 |
@override(from: String!) | SDL | Перенаправляет резолв поля в другой подграф. | Federation 2 |
@composeDirective(name: String!) | SDL | Указывает, что директива участвует в композиции схем. | Federation 2 |
@cacheControl(maxAge: Int, scope: CacheScope) | SDL / запрос | Управляет кэшированием (Apollo Engine, persisted queries). | Apollo Server |
@auth(requires: Role!) | SDL | Контроль доступа (часто кастомная реализация). | Hasura, PostGraphile, Apollo Plugins |
🔎 В SDL директивы не изменяют семантику выполнения напрямую — их поведение реализуется сервером. Например,
@cacheControlтребует подключенияapollo-server-plugin-response-cache.
📘 3. Структура документа GraphQL (Document Structure)
Документ (.graphql-файл или строка) — это набор определений. Порядок не важен, но должен соответствовать грамматике.
3.1. Типы определений
| Тип | SDL | Запрос | Описание |
|---|---|---|---|
SchemaDefinition | ✅ | ❌ | schema { query: Query, mutation: Mutation } — переопределяет имена корневых типов (по умолчанию Query, Mutation, Subscription). |
TypeDefinition | ✅ | ❌ | type, interface, union, scalar, enum, input — объявление типов. |
TypeExtension | ✅ | ❌ | extend type User { ... } — расширение существующего типа (должен быть объявлен ранее или в другом файле). |
DirectiveDefinition | ✅ | ❌ | directive @auth(...) on FIELD_DEFINITION — объявление новой директивы. |
OperationDefinition | ❌ | ✅ | query, mutation, subscription — корневой запрос. Может быть именованным или анонимным (но не более одного анонимного в документе). |
FragmentDefinition | ❌ | ✅ | fragment UserFields on User { ... } — именованный фрагмент. |
ExecutableDirective | ❌ | ✅ | Директивы, используемые в запросах (@include, @skip, @defer). |
3.2. Корневые операции
schema {
query: RootQuery # по умолчанию — Query
mutation: RootMutation # по умолчанию — Mutation
subscription: Events # по умолчанию — Subscription
}
Правила:
query— обязательно (даже если не объявлен явно → подразумеваетсяQuery).mutationиsubscription— опциональны. Если не объявлены — мутации и подписки запрещены.
📘 4. Аргументы (Arguments)
Аргументы могут быть у:
- полей объектов и интерфейсов
- директив (в SDL и запросах)
- корневых операций (редко, но возможно)
4.1. Типы значений аргументов
| Контекст | Возможные значения |
|---|---|
| В SDL (при объявлении поля) | — Тип (обязательно) — Значение по умолчанию (опционально, только для скаляров, ENUM, массивов, input-объектов) |
| В запросе (литералы) | 42, "str", true, null, [1,2], { a: "b" }, ENUM_VALUE |
| В переменных | Любое JSON-совместимое значение, соответствующее типу переменной |
4.2. Значения по умолчанию в SDL
Поддерживаются следующие литералы:
| Тип | Примеры |
|---|---|
Int / Float | = 0, = -42, = 3.14 |
String | = "", = "default" |
Boolean | = true, = false |
Enum | = ADMIN, = RED |
ID | = "default_id" |
List | = [], = ["a", "b"] |
Input Object | = { name: "anon", tags: [] } |
❗ Нельзя использовать
nullкак значение по умолчанию в SDL. Чтобы поле былоnullпо умолчанию — не делайте его!, и не указывайте дефолт.
4.3. Переменные (Variables)
query GetUser($id: ID!, $limit: Int = 10) {
user(id: $id) {
posts(first: $limit) { id }
}
}
Правила:
- Имя:
$+/[_A-Za-z][_0-9A-Za-z]*/. - Тип переменной не может быть
Object,Interface,Union,Input Objectбез!или[]— только скаляры, ENUM, input-типы (возможно, обёрнутые). - Значения переменных передаются отдельно (в
variables: { ... }). - Если переменная объявлена как
!, но вvariables—nullили отсутствует — ошибка валидации до выполнения.
📘 5. Фрагменты (Fragments)
5.1. Именованные фрагменты
fragment UserBase on User {
id
name
}
query {
me { ...UserBase }
other: user(id: "2") { ...UserBase }
}
Особенности:
- Должен быть объявлен до первого использования в документе (лексический порядок).
- Может включать другие фрагменты (вложенность разрешена).
- Используется только на совместимых типах:
on User→ можно использовать дляUser, типов, реализующих интерфейсUser, или union-типов, содержащихUser.
5.2. Inline-фрагменты
{
node(id: "xyz") {
... on User {
name
}
... on Post {
title
}
}
}
Разновидности:
... on Type— условный фрагмент (для union/interface)....(безon) — «spread all» фрагмент (всё, что совместимо с текущим типом). Например, внутриUser:{
me {
... # эквивалентно ... on User
}
}
⚠️ Бесконечная рекурсия фрагментов (
fragment A on T { ...B },fragment B on T { ...A }) — ошибка валидации.
📘 6. Семантика выполнения (Execution Semantics)
6.1. Резолверы (Resolvers)
Функция-резолвер имеет сигнатуру (в большинстве реализаций):
(parent: any, args: Args, context: Context, info: GraphQLResolveInfo) => any | Promise<any>
parent— значение родительского поля (для корневых типов —rootValue, переданный вexecute).args— аргументы поля (распарсенные, с применением дефолтов).context— разделяемый объект (аутентификация, БД-соединение и т.д.).info— метаданные: поле, путь, AST, схема.
Правила по умолчанию (если резолвер не задан):
- Если
parent— объект с полем, совпадающим по имени — возвращаетсяparent[fieldName]. - Если
parent[fieldName]— функция без аргументов — вызывается и возвращается результат. - Иначе —
undefined→ интерпретируется какnull(если поле nullable) или ошибка (если!).
6.2. Обработка ошибок
- Ошибка в резолвере (throw) → добавляется в
errorsответа, поле становитсяnull(если nullable), иначе — подъём ошибки до ближайшего nullable предка. - Системные ошибки (парсинг, валидация, отсутствие резолвера для required поля) — останавливают выполнение до начала резолвинга.
Структура ошибки в ответе:
{
"errors": [
{
"message": "Cannot return null for non-nullable field User.name.",
"locations": [{ "line": 3, "column": 5 }],
"path": ["user", "name"],
"extensions": { ... }
}
],
"data": { "user": null }
}
📘 7. Интроспекция (Introspection)
GraphQL требует поддержки интроспекции — возможности запросить метаданные о самой схеме через стандартные поля и типы. Это основа для инструментов (GraphiQL, Apollo Studio, codegen).
7.1. Специальные поля
| Поле | Тип | Назначение |
|---|---|---|
__typename | String! | Возвращает имя типа текущего объекта. Доступно на любом объекте, интерфейсе, union’е. Обязательно для клиентов (например, Apollo Client требует его для normalisation). |
__schema | __Schema! | Возвращает полную схему (только в корне Query). |
__type(name: String!) | __Type | Возвращает информацию о конкретном типе по имени. |
7.2. Стандартные типы интроспекции
__Schema
type __Schema {
description: String # (не в спецификации, но часто добавляется)
types: [__Type!]! # все именованные типы
queryType: __Type! # корневой тип Query
mutationType: __Type # nullable
subscriptionType: __Type # nullable
directives: [__Directive!]!
}
__Type
type __Type {
kind: __TypeKind!
name: String
description: String
# Для OBJECT, INTERFACE, INPUT_OBJECT
fields(includeDeprecated: Boolean = false): [__Field!]
# Для OBJECT, INTERFACE
interfaces: [__Type!]
# Для OBJECT, UNION
possibleTypes: [__Type!]
# Для ENUM
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
# Для INPUT_OBJECT
inputFields: [__InputValue!]
# Для INTERFACE, UNION, ENUM, INPUT_OBJECT
ofType: __Type
# Federation: используется в composition
specifiedByUrl: String
isOneOf: Boolean # для input объектов с @oneOf (Federation 2.3+)
}
__TypeKind (enum)
| Значение | Описание |
|---|---|
SCALAR | Int, String, DateTime |
OBJECT | User, Post |
INTERFACE | Node, SearchResult |
UNION | `SearchResult = User |
ENUM | UserRole |
INPUT_OBJECT | UserInput |
LIST | [String] — не именованный, ofType указывает на элемент |
NON_NULL | String! — не именованный, ofType указывает на обёртываемый тип |
__Field
type __Field {
name: String!
description: String
args: [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}
__InputValue
type __InputValue {
name: String!
description: String
type: __Type!
defaultValue: String # сериализованное значение (например, "10", "\"default\"")
isDeprecated: Boolean!
deprecationReason: String
}
🔎
defaultValue— строка, содержащая SDL-литерал. Парсится клиентом по тем же правилам, что и аргументы. Например:"[]","{ name: \"anon\" }".
__Directive
type __Directive {
name: String!
description: String
locations: [__DirectiveLocation!]!
args: [__InputValue!]!
isRepeatable: Boolean! # можно ли использовать директиву несколько раз на одном элементе
}
__DirectiveLocation (enum)
| Группа | Значения |
|---|---|
| Executable | QUERY, MUTATION, SUBSCRIPTION, FIELD, FRAGMENT_DEFINITION, FRAGMENT_SPREAD, INLINE_FRAGMENT, VARIABLE_DEFINITION |
| Type System | SCHEMA, SCALAR, OBJECT, FIELD_DEFINITION, ARGUMENT_DEFINITION, INTERFACE, UNION, ENUM, ENUM_VALUE, INPUT_OBJECT, INPUT_FIELD_DEFINITION |
💡
isRepeatable: trueпозволяет:type User {
id: ID! @tag(name: "core") @tag(name: "pii")
}
📘 8. Подписки (Subscriptions)
Подписки позволяют серверу отправлять данные клиенту асинхронно в ответ на события.
8.1. Объявление в схеме
type Subscription {
postPublished(topic: String!): Post!
userConnected(id: ID!): UserStatus!
}
8.2. Семантика выполнения
- Клиент отправляет
subscription { ... }. - Сервер немедленно возвращает
data: null, extensions: { ... }и устанавливает подключение (часто через WebSocket). - При возникновении события сервер отправляет инкрементальный ответ:
{ "data": { "postPublished": { "id": "123", "title": "..." } } } - Подписка прекращается при:
complete-сообщении от клиента,- ошибке,
- таймауте (опционально),
- закрытии соединения.
8.3. Протоколы транспорта
| Протокол | Поддержка | Примечания |
|---|---|---|
GraphQL over WebSocket (graphql-ws) | Современный стандарт (The Guild) | Поддерживает @defer, @stream, next/error/complete сообщения. Заменяет subscriptions-transport-ws. |
| SSE (Server-Sent Events) | Простой HTTP-поток | Только от сервера к клиенту. Подходит для read-only подписок. Не поддерживает GQL_START/GQL_STOP — управление через Last-Event-ID. |
| MQTT/STOMP поверх WebSocket | В enterprise-средах | Редко, но используется в IoT или legacy. |
⚠️ HTTP long-polling не рекомендуется — нарушает принципы GraphQL (stateless, idempotent).
8.4. Реализация резолвера
Резолвер подписки должен возвращать AsyncIterable (например, AsyncGenerator или EventEmitter-обёртку):
const resolvers = {
Subscription: {
postPublished: {
subscribe: (parent, args, ctx) =>
pubsub.asyncIterator(`POST_PUBLISHED.${args.topic}`),
resolve: (payload, args, ctx) => payload.post, // опционально
},
},
};
subscribe()— возвращаетAsyncIterable.resolve()— трансформирует payload перед отправкой клиенту (по умолчанию — identity).
📘 9. Federation (Apollo Federation)
Подход к созданию единой GraphQL-схемы из нескольких микросервисов (подграфов).
9.1. Ключевые концепции
| Понятие | Описание |
|---|---|
| Подграф (Subgraph) | Отдельный GraphQL-сервис, управляемый командой. Имеет свою SDL. |
| Supergraph | Единая виртуальная схема, сгенерированная композицией подграфов. |
| Composition | Процесс объединения SDL подграфов в supergraph (выполняется через rover supergraph compose). |
| Query Planner | Компонент routera, разбивающий запрос на подзапросы к подграфам. |
9.2. Обязательные директивы Federation 2
| Директива | Назначение | Пример |
|---|---|---|
@key(fields: String!) | Определяет primary key для типа. Разрешает расширение в других подграфах. | type User @key(fields: "id") { id: ID! } |
@extends | Указывает, что тип определён в другом подграфе. (Устарел в v2 — достаточно @key.) | — |
@external | Помечает поле как определённое в другом подграфе, но используемое здесь. | type Review { user: User! @external } |
@requires(fields: String!) | Запрашивает поля у родительского типа для резолвинга. | user: User @requires(fields: "userId") |
@provides(fields: String!) | Гарантирует, что поле будет разрешено в текущем подграфе и может использоваться другими. | author: User! @provides(fields: "name") |
9.3. Пример composition
Подграф users:
type User @key(fields: "id") {
id: ID!
name: String!
}
Подграф posts:
type User @key(fields: "id") {
id: ID!
posts: [Post!]! @shareable
}
type Post @key(fields: "id") {
id: ID!
title: String!
author: User!
}
Supergraph (результат):
type User @key(fields: "id") {
id: ID!
name: String!
posts: [Post!]!
}
type Post { ... }
🔍
@shareableразрешает определениеpostsв нескольких подграфах (например, для фильтрации по типу).
📘 10. Безопасность и защита
10.1. Ограничение глубины и ширины запроса
- Depth limiting — ограничение вложенности полей (например,
user { friends { friends { ... } } }). - Width limiting — ограничение количества полей на уровне.
- Cost analysis — оценка «стоимости» запроса на основе:
- сложности поля (1 — скаляр, 10 — список, 100 — внешний вызов),
- множителя пагинации (
first: 1000→ ×1000), - кэшируемости.
Реализации: graphql-depth-limit, graphql-cost-analysis, Envelop plugins.
10.2. Persisted Queries
- Клиент отправляет хэш запроса (например, SHA-256), а не полный текст.
- Сервер сверяет хэш с предварительно зарегистрированным списком.
- Преимущества:
- Защита от injection (нельзя отправить новый запрос),
- Снижение трафика,
- Возможность аудита и rate limiting по хэшу.
⚠️ Требует CI/CD-интеграции:
rover ingest→ отправка SDL + операций в Apollo Studio.
10.3. Rate limiting
- По
operationId(persisted queries), - По полю
__typename+ параметрам (например,user(id: "...")), - По IP + токену.
Инструменты: Apollo Studio Embedded Reporting, graphql-rate-limit.
📘 11. Инструменты и подходы к разработке
| Подход | Описание | Плюсы | Минусы |
|---|---|---|---|
| SDL-first | Сначала пишется .graphql-файл, затем генерируется код (resolvers, types). | Чёткий контракт, документированность, codegen. | Жёсткая связь, сложнее динамические схемы. |
| Code-first | Схема строится программно (NestJS, TypeGraphQL, GraphQL-JS). | Гибкость, инкапсуляция, TypeScript-first. | Риск расхождения кода и документации. |
| Schema stitching | Объединение схем через mergeSchemas (устаревшее). | Простота для небольших проектов. | Слабая изоляция, проблемы с дублированием, нет composition. |
| Federation | Современный подход к распределённым схемам. | Масштабируемость, ownership, incremental adoption. | Сложность настройки, требует CI/CD. |
📘 12. Рекомендации по проектированию схем
12.1. Именование
| Категория | Рекомендация |
|---|---|
| Типы | PascalCase: UserProfile, PaymentMethod |
| Поля/аргументы | camelCase: createdAt, maxResults |
| Enum значения | SCREAMING_SNAKE_CASE: ACTIVE, PENDING_APPROVAL |
| Input типы | Суффикс Input: CreateUserInput, PaginationInput |
| Payload-типы для мутаций | Суффикс Payload: CreateUserPayload { user: User, errors: [Error!] } |
12.2. Пагинация
Используйте Relay Cursor Connections Specification:
type UserConnection {
edges: [UserEdge!]!
nodes: [User!]! # удобный alias для edges.node
pageInfo: PageInfo!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
Аргументы: first, after, last, before.
❗ Избегайте
offset/limitдля больших данных — нет стабильного курсора.
12.3. Версионирование
GraphQL не рекомендует версионирование (/v1/graphql). Вместо этого:
- Добавляйте поля (бэквард-совместимо),
- Устаревайте через
@deprecated, - Используйte
@inaccessibleв Federation для постепенного удаления.